'''The declopt module provides a wrapper around optparse that allows quick,
declarative population of an optparse.OptionParser. It does not wrap all
the functionality of optparse. This is intentional. Some of the functionality
of optparse, such as the parse_args method of the OptionParser class are meant
to be used in conjunction with this library. Others are needed so rarely, such
as callbacks, that they did not fall under the purview of this library. Still
others are considered harmful to usability, in the vast majority of cases, and
are purposely shunned, such as nargs and append. The motivation behind this
wrapper is that even shunning and ignoring the above lesser features optparse is
still baroque and confusing by the very inclusion of these lesser features.

Hence, this wrapper designs to expose the more common and useful
features of optparse in a more easily digestible manner.

The motivation for declopt was simplicity. Command line parsing is a common
task, long ago solved, that should be as simple to implement as is it is to use.
optparse is simpler to use than its procedural father getopt, but it exhibits
a considerable extensibility fever that gets in the way of its usage for common
tasks. declopt tries to make these common tasks dirt simple.

declopt defines four functions: helps, options, group, and parser. Their exact
usage is left to their respective docstrings. What follows are illustrations of
their uses and examples of good practices.

As comparison, we define a simple OptionParser with two switches; first the
optparse way, and then the declopt way.

#the optparse way
from optparse import *
cmdparse = OptionParser(prog='example', version='0.1a',
                        description='utterly useless')
cmdparse.add_option('-q', '--quiet', action='store_const', const='quiet',
                    dest='verbosity', help='Display only errors')
cmdparse.add_option('-v', '--verbose', action='store_const', const='verbose',
                    dest='verbosity',
                    help='Display every little thing we can think of')
cmdparse.set_defaults(verbosity='quiet')
options, args = cmdparse.parse_args()

#the declopt way
from declopt import *
cmdparse = parser(program='example', version='0.1a',
                    description='utterly useless',
                    options(verbosity='q:quiet ; v:verbose  # quiet'),
                    helps({'q:quiet': 'Display only errors',
                           'v:verbose':
                                'Display every little thing we can think of'}))
options, args = cmdparse.parse_args()

In these examples the differences are minor, but in larger more realistic
situations the difference in LOC become more pronounced, as does the need to
slog through the 40 pages of optparse's documentation. Moreover, there is
notable redundancy in the optparse code that is eliminated (albeit tersely)
by the declopt code. Further, the parser function simply returns an instance of
OptionParser, so anything that declopt cannot do can be done the old fashioned
way, without requiring you to abandon declopt entirely.

parser takes any number of arguments after the required program, version, and
description. This can be any mixture of the outputs of the options, helps, and
group functions. There are also two special variables that can be used,
null_options and null_group. These are useful in cases like:

#Note that the keys defined will not show up when not in __debug__, the easiest
#way around this is to have an if __debug__ section in your code handling the
#parsed arguments.
if __debug__:
    debug_options = options(...)
else:
    debug_options = null_options

Note that any extra keys in the dictionary sent to helps() will be ignored, so
that in the above example a single call to helps can include keys for all
switches defined in debug_options without causing any errors.

group takes any number of outputs from the options function (or references to
null_options.) Unlike optparse.OptionGroup there's no need for circular
references in your code--it is all handled inside the parse function. Let us
again see this first the optparse way and then the declopt way.

#the optparse way
parser = optparse.OptionParser(...)
g = optparse.OptionGroup(parser, ...)
g.add_option(...)
...
parser.add_option_group(g)

#the declopt way
g = declopt.group(..., options(...))
parser = declopt.parser(..., g)

Now that we have demonstrated the usefulness of declopt, we present a more
thorough example to illustrate its usage and recommended best practices.

import declopt
# We assume that _ is installed from gettext
# In the following comments values refers to the dictionary returned
#  by parse_args as in values, args = the_parser.parse_args(sys.argv[1:])
def make_parser():
    help_db = helps({
                'v:verbose': _("Report everything"),
                'q:quiet': _("Report only errors"),
                'level': _("""Set the level of redundancy. Valid choices are
                0, 1, 2, or 3. The default is 0."""),
                'x': None, #if we left these undefined instead of setting them
                'y': None, #to None, they would be displayed on --help without
                'z': None, #any information, instead of not being shown at all
                'dump': "dump extra log info to console", 
                'log': "log extra info"})
    std_opts = declopt.options(
        # values.verbosity == True if -v/--verbose
        'verbosity':'v:verbose | q:quiet #quiet',
        # values.level is a /string/ in ('0', '1', '2', '3')
        # 'level=int#0' would be zero but would accept -1 or 200
        'level': 'level = 0, 1, 2, 3 #0')
    adv_opts = declopt.options(
        # note that quant was not defined in helps and will be displayed with
        # no description on --help
        quant='quantizer=complex',
        # create switches that don't have a default. special will be equal to
        # None (since there's no default) or 'x' or 'y' or 'z'
        special='x; y; z')

    if __debug__:
        # the help strings aren't translated with _() because presumably only
        # the programmers see them
        dbg_opts = declopt.options(debug='dump|log#log')
    else:
        # note that descriptions of dump and log won't show up now on --help
        dbg_opts = declopt.null_options

    adv_group = declopt.group(_("Advanced options"),
                _("Do not use these unless you know what you're doing!"),
                adv_opts, dbg_opts)

    #we're assuming that __version__ is defined somewhere else in the module
    #description[0] appears before the switches and description[1] after
    description = [_("No, really, it doesn't do anything or make any sense"),
                    _("See, what did I tell you?")]
    return declopt.parser(_("A useless program"), __version__, description,
            std_opts, adv_group, help_db)

It is left as an exercise to the reader to translate this into the optparse way.

Some technical notes:

* There are no classes. All data is stored in plain, but nested, Python objects.
    A brief explanation is given below to aid debugging.
    * helps(...) ->
        ('help', {dict of help strings})
        * 'help' is an internal tag that tells parser how to process this data.
        * Note that the dict keys aren't the specified 'c:str' but 
            ('-c','--str') and that None has been replaced by the value of
            optparse.SUPPRESS_HELP
    * options(...) ->
        ('options', ([(*args, **kwargs), ...], {key=default,...}) )
        * 'options' is an internal tag.
        * There is a list of tuples that are expanded to the positional and
            keyword arguments of optparse's add_option
        * a dictionary of default keys, where key is the key specified
            to options
    * group(..) ->
        ('group', ({group kwargs}, [same as options], {defaults}) )
        * very similar to the above, with a different tag and an extra
            dictionary to hold the title and description of the group.

* There is no error handling. This can result in bizarre exceptions and
    behavior. It is recommended that as you test offending options in the Python
    interpreter.'''

__all__ = ['helps', 'options', 'null_options', 'group', 'null_group', 'parser']

import optparse

_sfmt, _lfmt = lambda s: '-%s' % s, lambda s: '--%s' % s

_cleansplit = lambda string, on: [s.strip() for s in string.split(on)]

def _fmt(colon):
    if ':' not in colon:
        if len(colon) == 1:
            return (_sfmt(colon), '')
        else:
            return ('', _lfmt(colon))
    else:
        sfmtd, lfmtd = _cleansplit(colon, ':')
        return _sfmt(sfmtd), _lfmt(lfmtd)

def _longest_name(sname):
    names = _cleansplit(sname, ':')
    return names[len(names) - 1]

_id = lambda value: value
_coermap = {'int': int, 'long': long, 'float': float, 'complex': complex,
            'string': _id, 'choice': _id}

def helps(help):
    '''Define help strings for the parser function. It takes one argument,
    a dictionary, where the keys should match the SNAMES (see options docsting)
    of switches. To suppress the display of a switch on calls to --help, use
    the special None value instead of a string.

    Example:
    o = options(log_level='none; e:errors; all; mystery_opt')
    h = helps({ 'none': _('Do not log anything'),
                'e:errors': _('Log only errors'),
                'all': None})
                #mystery_opt will be displayed but with no help string'''
    real_help = {}
    for key, val in help.items():
        fkey = _fmt(key)
        if val is None:
            real_help[fkey] = optparse.SUPPRESS_HELP
        else:
            real_help[fkey] = val
    return ('help', real_help)

def options(**decls):
    '''Generate a list of options for the group or parser functions, using
    a simple declarative language to specify both the names
    of the switches and how they should be handled by OptionParser
    (hereafter, OP.)

    The language is a keyword argument (key=value) where the key is the 'dest'
    attribute of the OP and the value is a string describing the switch
    or a tuple starting with such a string and followed by one or more
    help strings or a sentinel None. The grammar of the code string will be
    explained before discussing how to specify the help strings.

    Note that all spaces between terminals will be removed.

    The basic element of the code string is a switch name that can be of three
    forms (where c is a single character and str is a single string) :
    1. 'c'. Just a short name, set as '-c' in the OP.
    2. 'str'. Just a long name, set as '--str' in the OP.
    3. 'c:str'. Both a short and long name, each saved as in (1) and (2).
    In the following explanation of the grammar, SNAME will refer to a string
    in one of these three forms. Note that for (1) 'c' is the SNAME's longest
    name and for (2) and (3) 'str' is the SNAME's longest name.

    There are three kinds of code strings. All of which support default values,
    which are to be discussed after the three forms.

    The simplest code string corresponds to action='store' of the OP. It has
    two forms:
    1. key='SNAME'. Store a string in the OP at key, with 'metavar' set to the
            string value of key in uppercase.
    2. key='SNAME=stype'. Same as (1) except the parsed value will be stored
            as stype in the OP. See the OP documentation for valid types.
    3. key='SNAME=str_0,str_1,...,str_N'. Same as (2) except the parsed value
            must match one of the N strings specified. The type will be set
            to choice.

    The next kind of code string represents Boolean flags. It has one form,
    key='SNAME|SNAME', where the leftmost SNAME sets the true flag and
    the rightmost sets the false flag.

    The final kind of code string is of the form 'SNAME_0;SNAME_1;...;SNAME_N'
    where each 'const' is the long name in SNAME if present and the short name
    otherwise. This is the same as action='store_const' in the OP. For example,
    'a:all; b; :con' would be 'all' for -a or --all, 'b' for -b, and 'con' for
    --con.

    For any of the three code strings, a default value can be specified after
    the main part of the declaration with a '#', such as: 'SNAME=TYPE#DEFAULT',
    'SNAME|SNAME#DEFAULT', and 'SNAME;SNAME;...;SNAME#DEFAULT'. Note that for
    the last two the default value must be the longest name in one of its
    SNAME's.

    See the module level documentation for further explanations, examples,
    and uses.'''
    defaults, optlist = {}, []
    has_default = False
    add_option = lambda snl, k: optlist.append((_fmt(snl), k))

    for key, val in decls.items():

        #first remove defaults
        if '#' in val:
            code, defaults[key] = _cleansplit(val, '#')
            has_default = True
        else:
            code = val
            has_default = False

        #parse based on existence of lexemes
        if ';' in code: #const
            codes = _cleansplit(code, ';')
            for descs in codes:
                const = _longest_name(descs)
                kwa = {'action': 'store_const', 'const': const, 'dest': key}
                add_option(descs, kwa)
                #note that the default is already stored as long as it is
                #the same as the longest name of the selected default.
                #this is not checked

        elif '|' in code: #Boolean
            top, fop = _cleansplit(code, '|')
            tkwa = {'dest': key, 'action': 'store_true'}
            fkwa = {'dest': key, 'action': 'store_false'}
            if has_default: #we need to set the default to either True or False
                defaults[key] = _longest_name(top) == defaults[key]
            add_option(top, tkwa)
            add_option(fop, fkwa)

        else: #name and optional type or choices
            desc_type = _cleansplit(code, '=')
            kwa = {'dest': key, 'metavar': key.upper()}
            type_check = 'string'
            if len(desc_type) == 2:
                stype = desc_type[1]
                if ',' in stype: #choice
                    choices = _cleansplit(stype, ',')
                    type_check = kwa['type'] = 'choice'
                    kwa['choices'] = choices
                else: #type
                    type_check = kwa['type'] = stype
            if has_default:
                defaults[key] = _coermap[type_check](defaults[key])
            add_option(desc_type[0], kwa)

    return 'options', (optlist, defaults)

null_options = ('options', None)

def group(title, description, *opt_list):
    '''group(title, description, opts) defines a group of options with a title
    and description for the end user's convenience.

    For example:
    advanced = group('Advanced options', 'Use these at your own risk', adv_opts)
    where adv_opts references the output of a call to options.

    See OptionParser.add_group for more information on OptionGroup's and
    this module's documentation for further information on usage.'''
    opts, defs = [], {}
    for tag, item in opt_list:
        if tag is not None:
            inopts, indefs = item
            opts.extend(inopts)
            defs.update(indefs)
    return 'group', ({'title': title, 'description': description}, (opts, defs))

null_group = ('group', None)

def parser(program, version, description, *opts):
    '''parser takes three fixed arguments followed by one or more positional
    arguments. The first argument is the name of the program. The second is
    the version of the program. The third is a short description. If the
    description is a list the first element will appear before the switches
    on help and the second after. The positional arguments are each the output
    of options, group, or helps.

    For example:
    cmdparse = parser('Moo', '1.3.5rc4', 'cowsay on steroids', stdopts, advopts)
    where stdopts and advopts are each either the output of options or group.

    Returns an instance of OptionParser. See its documentation for further
    information and this module's documentation for usages.'''
    epilog=None
    if not isinstance(description, basestring):
        description, epilog = description
    realp = optparse.OptionParser(prog=program, version=version,
                                  description=description, epilog=epilog)
    defaults, help_strings, real_options, real_groups = {}, {}, [], []

    def build(prsr, opts):
        for args, kwargs in opts:
            if args in help_strings:
                kwargs['help'] = help_strings[args]
            prsr.add_option(*args, **kwargs)

    #sort by tag and build help_strings
    for kind, items in opts:
        if kind == 'help':
            help_strings.update(items)
        elif kind == 'options' and items is not None:
            real_options.append(items)
        elif kind == 'group' and items is not None:
            real_groups.append(items)

    for opts, defs in real_options:
        build(realp, opts)
        defaults.update(defs)

    for tnd, (opts, defs) in real_groups:
        grp = optparse.OptionGroup(realp, **tnd)
        build(grp, opts)
        defaults.update(defs)
        realp.add_option_group(grp)

    realp.set_defaults(**defaults)
    return realp

